package org.zebra.web.advice;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.zebra.common.core.R;
import org.zebra.common.core.Result;
import org.zebra.common.exception.Failed;
import org.zebra.common.exception.ResultException;
import org.zebra.common.exception.StatusCode;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * 全局异常处理.
 *
 * @author zhanghongbin
 */
@Slf4j
@ControllerAdvice
@Order(200)
public class GlobalExceptionAdvice {

    private static Result<?> error(ResultException resultException) {
        return Result.builder()
                .code(resultException.getCode())
                .msg(resultException.getMsg())
                .build();
    }

    private static Result<?> error(StatusCode statusCode, Object obj) {
        return Result.builder()
                .code(statusCode.getCode())
                .msg(statusCode.getMsg())
                .data(obj)
                .build();
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseBody
    public Result<?> noHandlerFoundException(Exception ex) {
        return R.error(StatusCode.NOT_FOUND);
    }

    @ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
    @ResponseBody
    public Result<?> paramsException(Exception e) {
        if (e instanceof MethodArgumentNotValidException || e instanceof BindException) {
            return this.handleBindException((BindException) e);
        }
        if (e instanceof ConstraintViolationException) {
            return this.handleConstraintViolationException(e);
        }
        return R.error(StatusCode.PARAMS_CHECK);
    }

    private Result<?> handleBindException(BindException e) {
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        // String msg =
        // allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
        List<JSONObject> paramList = new ArrayList<>();
        allErrors.forEach(error -> {
            String str = CharSequenceUtil.subAfter(error.getArguments()[0].toString(), "[", true);
            String key = str.substring(0, str.length() - 1);
            String msg = error.getDefaultMessage();
            JSONObject param = new JSONObject();
            param.put("errorKey", key);
            param.put("errorMsg", msg);
            paramList.add(param);
        });
        return error(StatusCode.PARAMS_CHECK, paramList);
    }

    private Result<?> handleConstraintViolationException(Exception e) {
        ConstraintViolationException exception = (ConstraintViolationException) e;
        Set<ConstraintViolation<?>> violationSet = exception.getConstraintViolations();
        List<JSONObject> paramList = new ArrayList<>();
        violationSet.forEach(violation -> {
            String errorkey = violation.getPropertyPath().toString();
            String errorHintMsg = violation.getMessage();
            JSONObject param = new JSONObject();
            param.put("errorKey", errorkey);
            param.put("errorMsg", errorHintMsg);
            paramList.add(param);
        });
        return error(StatusCode.PARAMS_CHECK, paramList);
    }

    /**
     * 数据库操作异常
     *
     * @param ex 异常类
     * @return 错误对象
     */
    @ExceptionHandler(DataAccessException.class)
    @ResponseBody
    public Result<?> handleDataAccessException(Exception ex) {
        return error(new ResultException(StatusCode.DB_OPERATION.getCode(), ex.getMessage()));
    }

    @ExceptionHandler(AsyncRequestTimeoutException.class)
    public ResponseEntity<Object> ss(Exception ex) {
        return new ResponseEntity<>("SSE connection timed out", HttpStatus.REQUEST_TIMEOUT);
    }

    /**
     * 异常处理逻辑.
     *
     * @param throwable 业务逻辑抛出的异常
     * @return 统一返回包装后的结果
     */
    @ExceptionHandler({Throwable.class})
    @ResponseBody
    public Result<?> exceptionHandler(Throwable throwable) {
        Result<?> result;
        if (throwable instanceof ResultException) {
            result = error((ResultException) throwable);
        } else {
            // 校验异常转自定义异常
            result = fromExceptionInstance(throwable);
        }
        return result;
    }

    private Result<?> fromExceptionInstance(Throwable throwable) {
        Class<? extends Throwable> clazz = throwable.getClass();
        Failed failed = clazz.getAnnotation(Failed.class);
        // 1.有@Fail注解，直接设置结果的状态
        if (failed != null) {
            return error(new ResultException(failed.code(), failed.msg()));
        }
        log.error(this.getClass().getName() + "引发的异常", throwable);
        String throwableMessage = throwable.getMessage();
        if (throwableMessage != null) {
            return error(new ResultException(StatusCode.UNKNOWN.getCode(), throwableMessage));
        } else {
            return R.error(StatusCode.UNKNOWN);
        }
    }
}
